test: cobertura unitaria (18 -> 81) y smoke test de UI en CI - #1
Merged
Conversation
added 4 commits
August 21, 2026 21:45
La lógica pura estaba atrapada como private dentro de clases que arrastran Android (AndroidViewModel, Context), lo que obligaba a Robolectric para probar un par de funciones sin dependencias. Se extraen a funciones top-level internal en su propio módulo; ningún call site cambia. - isNewer: de OtaViewModel a OtaInfo.kt - wmoToCondition: de OpenMeteoWeatherDataSource a WeatherCondition.kt - reconnectDelayMs: de TwUtilMcuDataSource (estaba inline en el retryWhen) De paso corrige el back-off: attempt es Long y se narrowaba a Int antes de recortar, así que Long.MAX_VALUE.toInt() daba -1 y shl solo lee los 6 bits bajos del contador, produciendo un delay negativo que convertía la reconexión en un busy loop sobre el UART. Ahora se recorta como Long antes de bajar a Int. 63 tests nuevos: 18 -> 81.
Conduce la app sobre adb y asierta contra el árbol de accesibilidad que Compose ya expone (uiautomator ve cada nodo Text con sus bounds), así que no hace falta tocar el código de la app ni añadir dependencias. Localiza por texto y saca el centro de los bounds en vez de usar coordenadas fijas, espera con polling hasta 15 s antes de rendirse, y al fallar imprime todo el texto visible más un screenshot en build/smoke/. La aserción del BACK va invertida a propósito: no existe ningún BackHandler en el proyecto, así que el botón del sistema cierra la Activity desde cualquier pantalla en vez de volver a la anterior. El script pasa mientras el bug siga ahí y avisa el día que se arregle, en lugar de quedarse en rojo permanente y que todos aprendan a ignorarlo. En CI corre sobre API 29 a 768x1024, el mismo layout que el head unit.
El arranque en frío en el emulador de CI tarda bastante más que en uno local ya caliente, así que el sleep 3 tras am start convertía la primera aserción en un fallo intermitente: el volcado del propio fallo mostraba km/h presente 5 s después de que wait_for_text se rindiera. Ahora espera a mResumedActivity hasta 60 s, y sube el timeout de las aserciones a 30 s — cada poll cuesta un ui_dump completo (~2 s en CI), así que 15 s eran apenas unos pocos intentos.
grep -q sale en cuanto encuentra la primera coincidencia, lo que manda SIGPIPE al adb que sigue escribiendo. Con set -o pipefail el estado de la tubería pasa a ser ese 141, así que la aserción fallaba justo cuando el texto SÍ estaba presente — de ahí que el volcado del propio fallo lo mostrara. Localmente no se reproducía porque el dump es pequeño y adb termina de escribir antes de que grep cierre la tubería; en CI el volcado tarda más y el SIGPIPE sí ocurre. Verificado aparte: yes 'x' | head -200000 | grep -qF x -> PIPESTATUS = 141 141 0 Se sustituye por case sobre la captura, en wait_for_text/sees_now y en la espera de mResumedActivity de launch().
Joshumpa
approved these changes
Aug 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sube la cobertura de tests de 18 a 81 y añade un smoke test de UI que corre en CI.
Tests unitarios
La lógica que valía la pena probar estaba
privatedentro de clases que arrastran Android(
AndroidViewModel,Context), lo que obligaba a meter Robolectric para ejercitar un parde funciones que no dependen de nada. Se extraen a funciones top-level
internalen supropio módulo. Ningún call site cambia y no se añade ninguna dependencia.
OtaVersionTest(:ota)vdel tag, sufijos-rc1/+42, y que untag_namebasura ("",latest,nightly) nunca dispare una instalaciónWmoConditionTest(:sdk)CLOUDYen vez de dejar el home en blancoReconnectBackoffTest(:sdk):otano tenía wiring de tests; se le añade el mismo que ya usa:sdk.Un bug real en el back-off
attemptesLongy se narrowaba aIntantes de recortar:Con un
attemptgrande,toInt()desborda a negativo,coerceAtMost(6)deja pasar elnegativo, y
shlen Kotlin solo lee los 6 bits bajos del contador — así queshl -1esshl 63, el resultado desborda yminOfelige el valor negativo. Un delay negativoconvierte la reconexión al MCU en un busy loop sobre el UART.
Hacen falta ~2^31 reintentos para llegar ahí, o sea milenios a 30 s por intento: es
inalcanzable en la práctica. Se corrige igual porque cuesta cero, recortando como
Longantes de bajar a
Int.Smoke test de UI
tools/smoke.shconduce la app sobre adb y asierta contra el árbol de accesibilidad queCompose ya expone —
uiautomator dumpve cada nodoTextcon susbounds. Sindependencias nuevas y sin tocar el código de la app.
rompen al mover el layout.
build/smoke/.15 checks: arranque, home, Ajustes, selector de temas, relanzado y logcat sin crashes.
En CI corre en un job aparte con
reactivecircus/android-emulator-runner, sobre API 29 a768x1024 — la misma API y el mismo layout que el head unit real. Reutiliza el APK que ya
sube el job
build, y publica los screenshots como artifact pase o falle.Encontró un bug de navegación
En la primera corrida falló al volver de Apariencia. No existe ni un
BackHandlerentodo el proyecto, así que el botón BACK del sistema nunca toca la variable
screendeHelmLaunchery termina la Activity desde cualquier pantalla: desde Ajustes no vuelve alHome, se sale de la app.
Esa aserción queda invertida a propósito: espera el comportamiento roto, pasa mientras
el bug siga ahí, e imprime un
NOTEen cuanto alguien meta elBackHandler. Un test rojopermanente es un test que todo el mundo aprende a ignorar; el arreglo en sí va en otro PR.
Dos fallos de infraestructura en el camino
El job del emulador tardó tres corridas en quedar verde, y ninguna de las dos causas era
la app:
pipefail+grep -q.grep -qsale en cuanto encuentra la primera coincidencia ymanda SIGPIPE al
adbque sigue escribiendo; conset -o pipefailel estado de latubería pasa a ser ese 141, así que la aserción fallaba justo cuando el texto sí
estaba — de ahí lo desconcertante de que el volcado del propio fallo lo mostrara.
Localmente nunca se reprodujo porque el dump es pequeño y
adbtermina de escribirantes de que
grepcierre. Verificado aislado:yes x | head -200000 | grep -qF x→PIPESTATUS = 141 141 0.Sustituido por
casesobre la captura en los tres puntos afectados.sleepfijo trasam start. El arranque en frío en el emulador de CI tarda muchomás que en uno local ya caliente. Ahora espera a
mResumedActivity.Lo que no cubre
Los previews vacíos en el selector de temas no los caza este script: el nodo existe y es
"visible", simplemente no dibuja nada. Eso solo lo ve un test de screenshot (Paparazzi o
Roborazzi), y con la Fase 3 de UX en curso las imágenes golden cambiarían en cada ajuste.
Mejor cuando el diseño se asiente.